/**
 * 将 URL 对象转化成字符
 * from https://github.com/nodejs/node/blob/96f06e6482b36e90687c9f767356020279f1bd9e/lib/url.js#L539
 */

const CHAR_HASH = 35; /* # */
const CHAR_QUESTION_MARK = 63; /* ? */
const CHAR_FORWARD_SLASH = 47; /* / */

const slashedProtocol = new Set([
	'http',
	'http:',
	'https',
	'https:',
	'ftp',
	'ftp:',
	'gopher',
	'gopher:',
	'file',
	'file:',
	'ws',
	'ws:',
	'wss',
	'wss:',
]);

const hexTable = new Array(256);
for (let i = 0; i < 256; ++i)
	hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase();

function encodeStr(str, noEscapeTable, hexTable) {
	const len = str.length;
	if (len === 0) return '';

	let out = '';
	let lastPos = 0;

	for (let i = 0; i < len; i++) {
		let c = str.charCodeAt(i);

		// ASCII
		if (c < 0x80) {
			if (noEscapeTable[c] === 1) continue;
			if (lastPos < i) out += str.slice(lastPos, i);
			lastPos = i + 1;
			out += hexTable[c];
			continue;
		}

		if (lastPos < i) out += str.slice(lastPos, i);

		// Multi-byte characters ...
		if (c < 0x800) {
			lastPos = i + 1;
			out += hexTable[0xc0 | (c >> 6)] + hexTable[0x80 | (c & 0x3f)];
			continue;
		}
		if (c < 0xd800 || c >= 0xe000) {
			lastPos = i + 1;
			out +=
				hexTable[0xe0 | (c >> 12)] +
				hexTable[0x80 | ((c >> 6) & 0x3f)] +
				hexTable[0x80 | (c & 0x3f)];
			continue;
		}
		// Surrogate pair
		++i;

		// This branch should never happen because all URLSearchParams entries
		// should already be converted to USVString. But, included for
		// completion's sake anyway.
		if (i >= len) throw new Error('ERR_INVALID_URI');

		const c2 = str.charCodeAt(i) & 0x3ff;

		lastPos = i + 1;
		c = 0x10000 + (((c & 0x3ff) << 10) | c2);
		out +=
			hexTable[0xf0 | (c >> 18)] +
			hexTable[0x80 | ((c >> 12) & 0x3f)] +
			hexTable[0x80 | ((c >> 6) & 0x3f)] +
			hexTable[0x80 | (c & 0x3f)];
	}
	if (lastPos === 0) return str;
	if (lastPos < len) return out + str.slice(lastPos);
	return out;
}

// These characters do not need escaping:
// ! - . _ ~
// ' ( ) * :
// digits
// alpha (uppercase)
// alpha (lowercase)
// prettier-ignore
const noEscapeAuth = [
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00 - 0x0F
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x10 - 0x1F
  0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, // 0x20 - 0x2F
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, // 0x30 - 0x3F
  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x40 - 0x4F
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 0x50 - 0x5F
  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60 - 0x6F
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0  // 0x70 - 0x7F
];

export default function urlFormat(params) {
	let auth = params.auth || '';
	if (auth) {
		auth = encodeStr(auth, noEscapeAuth, hexTable);
		auth += '@';
	}

	let protocol = params.protocol || '';
	let pathname = params.pathname || '';
	let hash = params.hash || '';
	let host = '';
	let query = '';

	if (params.host) {
		host = auth + params.host;
	} else if (params.hostname) {
		host =
			auth +
			(params.hostname.includes(':')
				? '[' + params.hostname + ']'
				: params.hostname);
		if (params.port) {
			host += ':' + params.port;
		}
	}

	if (params.query !== null && typeof params.query === 'object') {
		query = new URLSearchParams(params.query).toString();
	}

	let search = params.search || (query && '?' + query) || '';

	if (protocol && protocol.charCodeAt(protocol.length - 1) !== 58 /* : */)
		protocol += ':';

	let newPathname = '';
	let lastPos = 0;
	for (let i = 0; i < pathname.length; ++i) {
		switch (pathname.charCodeAt(i)) {
			// case CHAR_HASH:
			// 	if (i - lastPos > 0) newPathname += pathname.slice(lastPos, i);
			// 	newPathname += '%23';
			// 	lastPos = i + 1;
			// 	break;
			case CHAR_QUESTION_MARK:
				if (i - lastPos > 0) newPathname += pathname.slice(lastPos, i);
				newPathname += '%3F';
				lastPos = i + 1;
				break;
		}
	}
	if (lastPos > 0) {
		if (lastPos !== pathname.length)
			pathname = newPathname + pathname.slice(lastPos);
		else pathname = newPathname;
	}

	// Only the slashedProtocols get the //.  Not mailto:, xmpp:, etc.
	// unless they had them to begin with.
	if (params.slashes || slashedProtocol.has(protocol)) {
		if (params.slashes || host) {
			if (pathname && pathname.charCodeAt(0) !== CHAR_FORWARD_SLASH)
				pathname = '/' + pathname;
			host = '//' + host;
		} else if (
			protocol.length >= 4 &&
			protocol.charCodeAt(0) === 102 /* f */ &&
			protocol.charCodeAt(1) === 105 /* i */ &&
			protocol.charCodeAt(2) === 108 /* l */ &&
			protocol.charCodeAt(3) === 101 /* e */
		) {
			host = '//';
		}
	}

	search = search.replace(/#/g, '%23');

	if (hash && hash.charCodeAt(0) !== CHAR_HASH) hash = '#' + hash;
	if (search && search.charCodeAt(0) !== CHAR_QUESTION_MARK)
		search = '?' + search;

	return protocol + host + pathname + search + hash;
}
